#include <regex>
#include "IniFile.h"
using namespace std;
using namespace inilib;


// -----------------------------------------------------------------------
//
// Constructors/Destructor
//
// -----------------------------------------------------------------------


IniFile::IniFile ( IniInfo * format, ValidationType type, std::istream & is )
{
	m_Format = format;
	m_ValidType = type;

	int lineID = 0;
	string line, comment = "", currSection = "";
	while ( getline( is, line ) )
	{
		++lineID;
		
		line = Trim( line );
		if ( line == "" )
		{
			continue;
		}
		if ( IsComment( line ) )
		{
			comment += "\n";
			comment += line;
			continue;
		}

		if ( IsNewSection( line ) )
		{
			if ( !IsEmpty( currSection ) )
			{
				ValidateSection( currSection );
			}

			// parse section:
			string newSection, newComment;
			ParseSection( line, lineID, newSection, newComment );
			if ( !IsEmpty( newComment ) )
			{
				comment += "\n";
				comment += newComment;
			}

			AddNewSection( newSection, comment, lineID, true );

			currSection = newSection;
			comment = "";
		}
		else
		{
			if ( IsEmpty( currSection ) )
			{
				stringstream ss;
				ss << "Option on line " << lineID << " doesn't belong to any section!";
				throw ExceptionIniLib( ss.str() );
			}

			// parse option:
			string values;
			string optionName, newComment;
			ParseOption( line, lineID, optionName, values, newComment );
			if ( !IsEmpty( newComment ) )
			{
				comment += "\n";
				comment += newComment;
			}

			AddNewOption( currSection, optionName, values, comment, lineID, true );

			comment = "";
		}
	}

	if ( !IsEmpty( currSection ) )
	{
		ValidateSection( currSection );
	}
	CheckMandatory();
	ValidateReferences();
}

IniFile::~IniFile()
{
	delete m_Format;
}


IniFile * IniFile::Create ( IniInfo * format, ValidationType type, const std::string & fileData )
{
	stringstream ss( fileData );
	return new IniFile( format, type, ss );
}


IniFile * IniFile::Create ( IniInfo * format, ValidationType type, std::istream & stream )
{
	return new IniFile( format, type, stream );
}


// -----------------------------------------------------------------------
//
// Save
//
// -----------------------------------------------------------------------


void IniFile::SaveTo ( std::string & s ) const
{
	stringstream ss;
	SaveTo( ss );
	s = ss.str();
}


void IniFile::SaveTo ( std::ostream & stream ) const
{
	for ( size_t i = 0; i < m_SectionsOrder.size(); ++i )
	{
		// get section:
		const string & sectionName = m_SectionsOrder[ i ];
		auto section = GetSection( sectionName );
		if ( !section.m_FromIni )
		{
			continue;
		}

		// save section name:
		if ( !IsEmpty( section.m_Comment ) )
		{
			stream << section.m_Comment << "\n";
		}
		stream << "[" << sectionName << "]\n";

		for ( size_t j = 0; j < section.m_OptionsOrder.size(); ++j )
		{
			// get option:
			const string & optionName = section.m_OptionsOrder[ j ];
			auto option = GetOption( sectionName, optionName );
			if ( !option.IsFromIni() )
			{
				continue;
			}

			// save option name:
			if ( !IsEmpty( option.GetComment() ) )
			{
				stream << option.GetComment() << "\n";
			}
			stream << optionName << " = ";
			

			// save all option values:
			char separator = option.GetSeparator();
			for ( size_t k = 0; k < option.GetAllValues().size(); ++k )
			{
				if ( k > 0 )
				{
					stream << " " << separator << " ";
				}
				stream << option.GetAllValues()[ k ];
			}
			stream << "\n";
		}
		stream << "\n\n";
	}
}


// -----------------------------------------------------------------------
//
// Exist
//
// -----------------------------------------------------------------------


bool IniFile::ExistSection ( const std::string & sectionName ) const
{
	auto iter = m_Sections.find( sectionName );
	return iter != m_Sections.end();
}


bool IniFile::ExistOption ( const std::string & sectionName, const std::string & optionName ) const
{
	if ( !ExistSection( sectionName ) )
	{
		return false;
	}

	auto iterSection = m_Sections.find( sectionName );
	auto iterOptions = iterSection->second.m_Options.find( optionName );
	return iterOptions != iterSection->second.m_Options.end();
}


// -----------------------------------------------------------------------
//
// Helper methods
//
// -----------------------------------------------------------------------


bool IniFile::IsEmpty ( const std::string & line ) const
{
	for ( size_t i = 0; i < line.size(); ++i )
	{
		if ( !isspace( line[ i ] ) )
		{
			return false;
		}
	}

	return true;
}


bool IniFile::IsComment ( const std::string & line ) const
{
	for ( size_t i = 0; i < line.size(); ++i )
	{
		if ( line[ i ] == ';' )
		{
			return true;
		}
		else if ( !isspace( line[ i ] ) )
		{
			return false;
		}
	}

	return false;
}


bool IniFile::IsNewSection ( const std::string & line ) const
{
	for ( size_t i = 0; i < line.size(); ++i )
	{
		if ( line[ i ] == '[' )
		{
			return true;
		}
		else if ( !isspace( line[ i ] ) )
		{
			return false;
		}
	}

	return false;
}


bool IniFile::IsValidChar ( char c ) const
{
	if (
		(c >= 'a' && c <= 'z') ||
		(c >= 'A' && c <= 'Z') ||
		(c >= '0' && c <= '9') ||
		c == '_' ||
		c == '~' ||
		c == '-' ||
		c == '.' ||
		c == ':' ||
		c == '$' ||
		c == ' '
		)
	{
		return true;
	}

	return false;
}


bool IniFile::IsValidIdentif ( const std::string & str ) const
{
	if ( str.empty() )
	{
		return false;
	}

	char first = str[ 0 ];
	bool firstValid = 
		(first >= 'a' && first <= 'z') ||
		(first >= 'A' && first <= 'Z') ||
		(first == '.') ||
		(first == ',') ||
		(first == '$') ||
		(first == ':');
	if ( !firstValid )
	{
		return false;
	}

	for ( size_t i = 1; i < str.size(); ++i )
	{
		if ( !IsValidChar( str[ i ] ) )
		{
			return false;
		}
	}

	return true;
}


std::string IniFile::RemoveSpaces ( const std::string & in ) const
{
	// get start and end:
	string whitespace = " \t\n\v\f\r\\";
	int start = in.find_first_not_of( whitespace );
	int end = in.find_last_not_of( whitespace );

	// add spaces from the beggining:
	string out = "";
	for ( int i = 0; i < start; ++i )
		if ( isspace( in[ i ] ) )
		{
			if ( i > 0 && in[ i - 1 ] == '\\' )
			{
				out += in[ i ];
			}
		}

	if ( start > -1 && end > -1 )
	{
		out += in.substr( start, end - start + 1 );
	}

	// add spaces from the end:
	if ( end > -1 )
	{
		for ( size_t i = end + 1; i < in.size(); ++i )
			if ( isspace( in[ i ] ) )
			{
				if ( in[ i - 1 ] == '\\' )
				{
					out += in[ i ];
				}
			}
	}

	return out;
}


std::string IniFile::Trim ( const std::string & str ) const
{
	string whitespace = " \t\n\v\f\r";
	
	int start = str.find_first_not_of( whitespace );
	if ( start == string::npos )
	{
		return "";
	}
	
	int end = str.find_last_not_of( whitespace );
	if ( end > 0 && str[ end - 1 ] == '\\' )
	{
		++end;
	}
	
	return str.substr( start, end - start + 1 );
}


// -----------------------------------------------------------------------
//
// Parse methods
//
// -----------------------------------------------------------------------


void IniFile::GetComment (
	const std::string & line,
	std::string & remainder,
	std::string & comment
) const
{
	int start = line.find( ';' );
	while ( start > 0 && line[ start - 1 ] == '\\' )
	{
		start = line.find( ';', start + 1 );
	}

	if ( start == string::npos )
	{
		comment = "";
		remainder = line;
	}
	else
	{
		comment = line.substr( start );
		remainder = line.substr( 0, start );
	}
}


void IniFile::ParseSection (
	const std::string & line,
	int lineNumber,
	std::string & sectionName,
	std::string & sectionComment
) const
{
	string remainder;
	GetComment( line, remainder, sectionComment );

	int start = remainder.find( '[' ) + 1;
	int end = remainder.find( ']', start ) - 1;
	sectionName = remainder.substr( start, end - start + 1 );

	if (
		( !IsEmpty( remainder.substr( 0, start - 1 )  ) ) ||
		( !IsEmpty( remainder.substr( end + 2 ) ) )
		)
	{
		stringstream ss;
		ss << "There is an invalid character in section \"" << sectionName << "\" on line " << lineNumber;
		throw ExceptionIniLib( ss.str() );
	}

	if ( !IsValidIdentif( sectionName ) )
	{
		stringstream ss;
		ss << "There is an invalid character in the name of the section \"" << sectionName << "\"";
		throw ExceptionIniLib( ss.str() );
	}
}


void IniFile::ParseOption (
	const std::string & line,
	int lineNumber,
	std::string & optionName,
	std::string & values,
	std::string & optionComment
) const
{
	string remainder;
	GetComment( line, remainder, optionComment );

	int eqPos = remainder.find( '=' );
	if (
		eqPos == 0 ||
		eqPos == string::npos
		)
	{
		stringstream ss;
		ss << "There is no '=' on line " << lineNumber << " (option is invalid)";
		throw ExceptionIniLib( ss.str() );
	}

	optionName = RemoveSpaces( remainder.substr( 0, eqPos ) );
	if ( remainder.size() == eqPos + 1 )
	{
		values = "";
	}
	else
	{
		values = remainder.substr( eqPos + 1 );
	}

	if ( !IsValidIdentif( optionName ) )
	{
		stringstream ss;
		ss << "There is an invalid character in the name of the option \"" << optionName
			<< "\" on line " << lineNumber;
		throw ExceptionIniLib( ss.str() );
	}
}


void IniFile::ParseReference (
	const std::string & line,
	std::string & sectionName,
	std::string & optionName
) const
{
	int posL = line.find( "{" );
	int posM = line.find( "#" );
	int posR = line.find( "}" );
	if (
		posL == string::npos ||
		posM == string::npos ||
		posR == string::npos ||
		posL > posM ||
		posL > posR ||
		posM > posR
		)
	{
		assert( false );
	}

	sectionName = line.substr( posL + 1, posM - posL - 1 );
	optionName = line.substr( posM + 1, posR - posM - 1 );
}


// -----------------------------------------------------------------------
//
// Add
//
// -----------------------------------------------------------------------


void IniFile::AddNewSection (
	const std::string & sectionName,
	const std::string & comment,
	int lineNumber,
	bool fromIni
)
{
	if ( ExistSection( sectionName ) )
	{
		stringstream ss;
		ss << "Section \"" << sectionName << "\" already exists (redefined on line " << lineNumber << ")";
		throw ExceptionIniLib( ss.str() );
	}

	m_SectionsOrder.push_back( sectionName );
	m_Sections.insert( make_pair( sectionName, Section( sectionName, comment, fromIni ) ) );
}


void IniFile::AddNewOption (
	const std::string & sectionName,
	const std::string & optionName,
	const std::string & allValues,
	const std::string & comment,
	int lineNumber,
	bool fromIni
)
{
	if ( ExistOption( sectionName, optionName ) )
	{
		stringstream ss;
		ss << "Option \"" << optionName << "\" already exists (redefined on line " << lineNumber << ")";
		throw ExceptionIniLib( ss.str() );
	}

	// get option info:
	char separator = ',';
	OptionType * optionInfo = nullptr;
	try
	{
		optionInfo = GetOptionType( sectionName, optionName );
		separator = optionInfo->GetSeparator();
	}
	catch ( const ExceptionIniLib & e )
	{
		if ( m_ValidType == STRICT )
		{
			throw e;
		}
	}

	// tokenize:
	vector< std::string > values;
	string str;
	stringstream ss( allValues );
	while ( std::getline( ss, str, separator ) )
	{
		values.push_back( RemoveSpaces( str ) );
	}

	AddNewOption( sectionName, optionName, values, comment, optionInfo, lineNumber, fromIni );
}


void IniFile::AddNewOption (
	const std::string & sectionName,
	const std::string & optionName,
	const std::vector< std::string > & values,
	const std::string & comment,
	OptionType * optionInfo,
	int lineNumber,
	bool fromIni
)
{
	// create option:
	char separator = DEFAULT_SEPARATOR;
	if ( optionInfo != nullptr )
	{
		separator = optionInfo->GetSeparator();
	}
	Option option( sectionName, optionName, comment, separator, fromIni );
	option.SetValues( values );

	// validate values:
	if ( optionInfo != nullptr )
	{
		if ( !optionInfo->AreValuesValid( option.GetValues(), option.GetAllValues().size() ) )
		{
			stringstream ss;
			ss << "Value(s) for option \"" << optionName << "\" in section \"" << sectionName << "\" are not valid";
			throw ExceptionIniLib( ss.str() );
		}
	}

	auto & section = GetSection( sectionName );
	section.m_OptionsOrder.push_back( optionName );
	section.m_Options.insert( make_pair( optionName, option ) );
}


void IniFile::ValidateSection ( const std::string & sectionName ) const
{
	// get Section & SectionInfo:
	auto & section = GetSection( sectionName );
	IniInfo::SectionInfo sectionInfo;
	try
	{
		sectionInfo = GetSectionInfo( sectionName );
	}
	catch ( const ExceptionIniLib & e )
	{
		if ( m_ValidType == STRICT )
		{
			throw e;
		}
		else
		{
			// section has no restraints:
			return;
		}
	}

	// validate Section against SectionInfo:
	auto iter = sectionInfo.m_Options.begin();
	while ( iter != sectionInfo.m_Options.end() )
	{
		const string & optionName = iter->first;
		bool isMandatory = iter->second->IsMandatory();
		if ( !isMandatory )
		{
			++iter;
			continue;
		}

		auto iterOptions = section.m_Options.find( optionName );
		if ( iterOptions == section.m_Options.end() )
		{
			stringstream ss;
			ss << "Section \"" << sectionName << " is not valid (option \"" << optionName << "\" is missing)";
			throw ExceptionIniLib( ss.str() );
		}

		++iter;
	}
}


void IniFile::CheckMandatory ()
{
	auto iterSI = m_Format->m_Sections.begin();
	while ( iterSI != m_Format->m_Sections.end() )
	{
		// check section:
		if ( !ExistSection( iterSI->first ) )
		{
			if ( iterSI->second.m_IsMandatory )
			{
				stringstream ss;
				ss << "Section \"" << iterSI->first << "\" is mandatory, but is it not in the ini file";
				throw ExceptionIniLib( ss.str() );
			}
			else
			{
				AddNewSection( iterSI->first, "", -1, false );
			}
		}

		// check options:
		auto iterOI = iterSI->second.m_Options.begin();
		while ( iterOI != iterSI->second.m_Options.end() )
		{
			if ( !ExistOption( iterSI->first, iterOI->first ) )
			{
				if ( iterOI->second->IsMandatory() )
				{
					stringstream ss;
					ss << "Option \"" << iterOI->first << "\" is mandatory in section \"" << iterSI->first
						<< "\", but it is not in the ini file";
					throw ExceptionIniLib( ss.str() );
				}
				else
				{
					vector< string > defValue = iterOI->second->GetDefaultValue();
					AddNewOption( iterSI->first, iterOI->first, defValue, "", nullptr, -1, false );
				}
			}

			++iterOI;
		}

		++iterSI;
	}
}


void IniFile::ResetStatus ()
{
	auto iterSec = m_Sections.begin();
	while ( iterSec != m_Sections.end() )
	{
		auto iterOpt = iterSec->second.m_Options.begin();
		while ( iterOpt != iterSec->second.m_Options.end() )
		{
			iterOpt->second.m_Status = NOT_VISITED;
			++iterOpt;
		}

		++iterSec;
	}
}


void IniFile::ValidateReferences ()
{
	ResetStatus();

	auto iterSec = m_Sections.begin();
	while ( iterSec != m_Sections.end() )
	{
		auto iterOpt = iterSec->second.m_Options.begin();
		while ( iterOpt != iterSec->second.m_Options.end() )
		{
			ValidateOptionRef( iterOpt->second );

			++iterOpt;
		}

		++iterSec;
	}
}


void IniFile::ValidateOptionRef ( Option & option )
{
	if ( option.m_Status == FINISHED )
	{
		return;
	}
	else if ( option.m_Status == IN_PROGRESS )
	{
		stringstream ss;
		ss << "There is a cycle in reference graph that ends at option \"" << option.GetOptionName() << "\"";
		throw ExceptionIniLib( ss.str() );
	}
	else if ( option.m_Status == NOT_VISITED )
	{
		option.m_Status = IN_PROGRESS;

		for ( size_t i = 0; i < option.GetReferences().size(); ++i )
		{
			string sectionName, optionName;
			ParseReference( option.GetReferences()[ i ], sectionName, optionName );

			auto & nextOption = GetOption( sectionName, optionName );
			ValidateOptionRef( nextOption );

			// all is valid (no cycle):
			vector< string > values;
			GetAllValues( nextOption, values );

			// validate:
			auto optionType = GetOptionType( option.GetSectionName(), option.GetOptionName() );
			if ( !optionType->AreValuesValid( values, values.size() ) )
			{
				stringstream ss;
				ss << "Value(s) of option \"" << option.GetOptionName() << "\" in section \""
					<< option.GetSectionName()
					<< "\" are not valid if the references are taken into account";
				throw ExceptionIniLib( ss.str() );
			}
		}

		option.m_Status = FINISHED;
	}
	else
	{
		// this is really weird:
		assert( false );
	}
}


// -----------------------------------------------------------------------
//
// Get/Set support methods
//
// -----------------------------------------------------------------------


IniFile::Section & IniFile::GetSection ( const std::string & sectionName ) const
{
	auto iter = m_Sections.find( sectionName );
	if ( iter == m_Sections.end() )
	{
		std::stringstream ss;
		ss << "Section \"" << sectionName << "\" does not exist";
		throw ExceptionIniLib( ss.str() );
	}

	return iter->second;
}


IniInfo::SectionInfo & IniFile::GetSectionInfo ( const std::string & sectionName ) const
{
	auto iter = m_Format->m_Sections.find( sectionName );
	if ( iter == m_Format->m_Sections.end() )
	{
		stringstream ss;
		ss << "Info for section \"" << sectionName << "\" does not exist";
		throw ExceptionIniLib( ss.str() );
	}

	return iter->second;
}


IniFile::Option & IniFile::GetOption (
	const std::string & sectionName,
	const std::string & optionName
) const
{
	auto iterSection = m_Sections.find( sectionName );
	if ( iterSection == m_Sections.end() )
	{
		std::stringstream ss;
		ss << "Section \"" << sectionName << "\" does not exist";
		throw ExceptionIniLib( ss.str() );
	}

	auto iterOption = iterSection->second.m_Options.find( optionName );
	if ( iterOption == iterSection->second.m_Options.end() )
	{
		std::stringstream ss;
		ss << "Option \"" << optionName << "\" in section \"" << sectionName << "\" does not exist";
		throw ExceptionIniLib( ss.str() );
	}

	return iterOption->second;
}


OptionType * IniFile::GetOptionType (
	const std::string & sectionName,
	const std::string & optionName
)
{
	auto sectionInfo = GetSectionInfo( sectionName );

	auto iterOptionInfo = sectionInfo.m_Options.find( optionName );
	if ( iterOptionInfo == sectionInfo.m_Options.end() )
	{
		stringstream ss;
		ss << "Info for option \"" << optionName << "\" in section \"" << sectionName << "\" does not exist";
		throw ExceptionIniLib( ss.str() );
	}

	return iterOptionInfo->second;
}


void IniFile::GetAllValues (
	Option & option,
	std::vector< std::string > & values
) const
{
	values = option.GetValues();
	for ( size_t i = 0; i < option.GetReferences().size(); ++i )
	{
		vector< string > tmp;
		string sectionName, optionName;
		ParseReference( option.GetReferences()[ i ], sectionName, optionName );

		auto nextOption = GetOption( sectionName, optionName );
		GetAllValues( nextOption, tmp );
		for ( size_t j = 0; j < tmp.size(); ++j )
		{
			values.push_back( tmp[ j ] );
		}
	}
}


// -----------------------------------------------------------------------
//
// Option class
//
// -----------------------------------------------------------------------


IniFile::Option::Option (
	const std::string & sectionName,
	const std::string & optionName,
	const std::string & comment,
	char separator,
	bool fromIni
)
{
	m_OptionName = optionName;
	m_SectionName = sectionName;
	m_Comment = comment;
	m_Separator = separator;
	m_FromIni = fromIni;
}


bool IniFile::Option::IsFromIni () const
{
	return m_FromIni;
}


char IniFile::Option::GetSeparator () const
{
	return m_Separator;
}


const std::string & IniFile::Option::GetComment () const
{
	return m_Comment;
}


const std::string & IniFile::Option::GetSectionName () const
{
	return m_SectionName;
}


const std::string & IniFile::Option::GetOptionName () const
{
	return m_OptionName;
}


const std::vector< std::string > & IniFile::Option::GetAllValues () const
{
	return m_AllValues;
}


const std::vector< std::string > & IniFile::Option::GetValues () const
{
	return m_Values;
}


const std::vector< std::string > & IniFile::Option::GetReferences () const
{
	return m_References;
}


void IniFile::Option::SetValues ( const std::vector< std::string > & values )
{
	m_AllValues = values;
	m_Values.clear();
	m_References.clear();
	for ( size_t i = 0; i < values.size(); ++i )
	{
		if ( IsReference( values[ i ] ) )
		{
			m_References.push_back( values[ i ] );
		}
		else
		{
			m_Values.push_back( values[ i ] );
		}
	}
}


bool IniFile::Option::IsReference ( const std::string & str ) const
{
	string whitespace = " \t\n\v\f\r";

	int start = str.find_first_not_of( whitespace );
	int end = str.find_last_not_of( whitespace );
	if (
		start != (int)string::npos &&
		end != (int)string::npos &&
		end - start + 1 >= 3
		)
	{
		if (
			str[ start ] == '$' &&
			str[ start + 1 ] == '{' &&
			str[ end ] == '}'
			)
		{
			if ( str.find( '#' ) != string::npos )
			{
				return true;
			}
		}
	}

	return false;
}
